import SEO from "../components/SEO";
import { TOC, TOCList, TOCLink } from "../components/TOC";

<SEO
	title="Dialog (Modal)"
	description="An accessible modal dialog component for React"
/>

# Dialog (Modal)

<TOC>
	<TOCList>
		<TOCLink href="#dialog">Dialog</TOCLink>
		<TOCLink href="#dialogoverlay">DialogOverlay</TOCLink>
		<TOCLink href="#dialoginner">DialogInner</TOCLink>
		<TOCLink href="#dialogcontent">DialogContent</TOCLink>
	</TOCList>
</TOC>

- Source: https://github.com/reach/reach-ui/tree/main/packages/dialog
- WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal

An accessible dialog or "modal" window.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Open Dialog</button>

			<Dialog isOpen={showDialog} onDismiss={close}>
				<button className="close-button" onClick={close}>
					<VisuallyHidden>Close</VisuallyHidden>
					<span aria-hidden>×</span>
				</button>
				<p>Hello there. I am a dialog</p>
			</Dialog>
		</div>
	);
}
```

## Installation

From the command line in your project directory, run `npm install @reach/dialog` or `yarn add @reach/dialog`. Then import the components and styles that you need:

```bash
npm install @reach/dialog
# or
yarn add @reach/dialog
```

```js
import { Dialog, DialogOverlay, DialogContent } from "@reach/dialog";
import "@reach/dialog/styles.css";
```

## Component API

### Dialog

High-level component to render a modal dialog window over the top of the page (or another dialog).

```jsx
<Dialog>
	<p>Some Content</p>
</Dialog>
```

#### Dialog Props

| Prop                                         | Type   | Required |
| -------------------------------------------- | ------ | -------- |
| [`div` props](#dialog-div-props)             |        |          |
| [`allowPinchZoom`](#dialog-allowpinchzoom)   | `bool` | false    |
| [`children`](#dialog-children)               | `node` | true     |
| [`initialFocusRef`](#dialog-initialfocusref) | `ref`  | false    |
| [`isOpen`](#dialog-isopen)                   | `bool` | false    |
| [`onDismiss`](#dialog-ondismiss)             | `func` | false    |

##### Dialog `div` props

Any props not listed above will be spread onto the underlying `DialogContent` element, which in turn is spread onto the underlying `div[data-reach-dialog-content]`.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>

			<Dialog style={{ color: "red" }} isOpen={showDialog} onDismiss={close}>
				<p>My text is red because the style prop got applied to the div</p>
				<button onClick={close}>Okay</button>
			</Dialog>
		</div>
	);
}
```

##### Dialog `allowPinchZoom`

`allowPinchZoom?: boolean`

Handle zoom/pinch gestures on iOS devices when scroll locking is enabled. Defaults to `false`.

```jsx
<Dialog allowPinchZoom={true}>
	<p>
		I will allow zoom and pinch gestures on iOS devices even when scrolling is
		locked!
	</p>
</Dialog>
```

##### Dialog `children`

`children: React.ReactNode`

Accepts any renderable content.

```jsx
<Dialog>
	<p>Anything you want, you got it.</p>
</Dialog>
```

##### Dialog `initialFocusRef`

`initialFocusRef?: React.RefObject<any>`

By default the first focusable element will receive focus when the dialog opens but you can provide a ref to focus instead.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const buttonRef = React.useRef();
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>
			<Dialog isOpen={showDialog} initialFocusRef={buttonRef} onDismiss={close}>
				<p>Pass the button ref to Dialog and the button.</p>
				<button onClick={close}>Not me</button>
				<button ref={buttonRef} onClick={close}>
					Got me!
				</button>
			</Dialog>
		</div>
	);
}
```

##### Dialog `isOpen`

`isOpen?: boolean`

Controls whether or not the dialog is open.

```jsx
<Dialog isOpen={true}>
  <p>I will be open</p>
</Dialog>

<Dialog isOpen={false}>
  <p>I will be closed</p>
</Dialog>
```

If you'd rather not have the dialog always rendered, you can put a guard in front of it and only render when it should be open. In this case you don’t need the `isOpen` prop at all.

Note, however, that the dialog will not render to the DOM when `isOpen={false}`, but you may want to save on the number of elements created in your render function. You should probably do this when your dialog contains a lot of elements.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>

			{showDialog && (
				<Dialog onDismiss={close}>
					<p>
						I don’t use <code>isOpen</code>, I just render when I should and not
						when I shouldn’t.
					</p>
					<button onClick={close}>Okay</button>
				</Dialog>
			)}
		</div>
	);
}
```

##### Dialog `onDismiss`

`onDismiss?(event: React.MouseEvent | React.KeyboardEvent): void`

This function is called whenever the user hits "Escape" or clicks outside the dialog. _It's important to close the dialog `onDismiss`_ as seen in all the demos on this page.

The only time you shouldn't close the dialog on dismiss is when the dialog requires a choice and none of them are "cancel". For example, perhaps two records need to be merged and the user needs to pick the surviving record. Neither choice is less destructive than the other, in these cases you may want to alert the user they need to a make a choice on dismiss instead of closing the dialog.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>

			<Dialog isOpen={showDialog} onDismiss={close}>
				<p>
					It is your job to close this with state when the user clicks outside
					or presses escape.
				</p>
				<button onClick={close}>Okay</button>
			</Dialog>
		</div>
	);
}
```

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const [showWarning, setShowWarning] = React.useState(false);
	const open = () => {
		setShowDialog(true);
		setShowWarning(false);
	};
	const close = () => setShowDialog(false);
	const dismiss = () => setShowWarning(true);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>

			<Dialog isOpen={showDialog} onDismiss={dismiss}>
				{showWarning && (
					<p style={{ color: "red" }}>You must make a choice, sorry :(</p>
				)}
				<p>Which router should survive the merge?</p>
				<button onClick={close}>React Router</button>{" "}
				<button onClick={close}>@reach/router</button>
			</Dialog>
		</div>
	);
}
```

### DialogOverlay

Low-level component if you need more control over the styles or rendering of the dialog overlay.

Note: You must render a `DialogContent` inside.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>
			<DialogOverlay
				style={{ background: "hsla(0, 100%, 100%, 0.9)" }}
				isOpen={showDialog}
				onDismiss={close}
			>
				<DialogContent
					style={{ boxShadow: "0px 10px 50px hsla(0, 0%, 0%, 0.33)" }}
				>
					<p>
						The overlay styles are a white fade instead of the default black
						fade.
					</p>
					<button onClick={close}>Very nice.</button>
				</DialogContent>
			</DialogOverlay>
		</div>
	);
}
```

#### DialogOverlay CSS Selectors

Please see the [styling guide](/styling).

Use the following CSS to target the overlay:

```css
[data-reach-dialog-overlay] {
}
```

#### DialogOverlay Props

| Prop                                                                 | Type   | Required |
| -------------------------------------------------------------------- | ------ | -------- |
| [`div` props](#dialogoverlay-div-props)                              |        |          |
| [`allowPinchZoom`](#dialogoverlay-allowpinchzoom)                    | `bool` | false    |
| [`children`](#dialogoverlay-children)                                | `node` | true     |
| [`dangerouslyBypassFocusLock`](#dialog-dangerouslybypassfocuslock)   | `bool` | false    |
| [`dangerouslyBypassScrollLock`](#dialog-dangerouslybypassscrolllock) | `bool` | false    |
| [`initialFocusRef`](#dialogoverlay-initialfocusref)                  | `ref`  | false    |
| [`isOpen`](#dialogoverlay-isopen)                                    | `bool` | false    |
| [`onDismiss`](#dialogoverlay-ondismiss)                              | `func` | false    |

##### DialogOverlay `div` props

Any props not listed above will be spread onto the underlying `div`.

```jsx
<DialogOverlay className="light-modal">
	<p>The underlying element will receive a class</p>
</DialogOverlay>
```

##### DialogOverlay `allowPinchZoom`

Same as [Dialog `allowPinchZoom`](#dialog-allowpinchzoom).

##### DialogOverlay children

`children: React.ReactNode`

Should be a `DialogContent` component.

```jsx
<DialogOverlay>
	<DialogContent>Hey!</DialogContent>
</DialogOverlay>
```

##### DialogOverlay `dangerouslyBypassFocusLock`

`dangerouslyBypassFocusLock?: boolean`

By default the dialog locks the focus inside it. Normally this is what you want. This prop is provided so that this feature can be disabled. This, however, is strongly discouraged.

The reason it is provided is not to disable the focus lock entirely. Rather, there are certain situations where you may need more control on how the focus lock works. This should be complemented by setting up a focus lock yourself that would allow more flexibility for your specific use case.

If you do set this prop to `true`, make sure you set up your own `FocusLock` component. You can likely use [`react-focus-lock`](https://github.com/theKashey/react-focus-lock), which is what Reach UI uses internally by default. It has various settings to allow more customization, but it takes care of a lot of hard work that you probably don't want or need to do.

```jsx
function DialogWithCustomFocusLock() {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);
	return (
		<div>
			<button onClick={open}>Show Dialog</button>
			<DialogOverlay
				isOpen={showDialog}
				onDismiss={close}
				dangerouslyBypassFocusLock
			>
				<MyCustomFocusLock>
					<DialogContent aria-label="Announcement">
						This is an important announcement!
					</DialogContent>
				</MyCustomFocusLock>
			</DialogOverlay>
		</div>
	);
}
```

##### DialogOverlay `dangerouslyBypassScrollLock`

`dangerouslyBypassScrollLock?: boolean`

By default the dialog locks scrolling with [`react-remove-scroll`](https://github.com/theKashey/react-remove-scroll), which also injects some styles on the body element to remove the scrollbar while maintaining its gap to prevent jank when the dialog's open state is toggled. This is almost always what you want in a dialog, but in some cases you may have the need to customize this behavior further.

This prop will disable `react-remove-scroll` and allow you to compose your own scroll lock component to meet your needs. Like the `dangerouslyBypassFocusLock` prop, this is generally discouraged and should only be used if a proper fallback for managing scroll behavior is provided.

```jsx
function DialogWithCustomScrollLock() {
	const [showDialog, setShowDialog] = React.useState(false);
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);
	return (
		<div>
			<button onClick={open}>Show Dialog</button>
			<DialogOverlay
				isOpen={showDialog}
				onDismiss={close}
				dangerouslyBypassScrollLock
			>
				<MyCustomScrollLock>
					<DialogContent aria-label="Announcement">
						This is an important announcement!
					</DialogContent>
				</MyCustomScrollLock>
			</DialogOverlay>
		</div>
	);
}
```

##### DialogOverlay `initialFocusRef`

Same as [Dialog `initialFocusRef`](#dialog-initialfocusref).

##### DialogOverlay `isOpen`

Same as [Dialog `isOpen`](#dialog-isopen).

##### DialogOverlay `onDismiss`

Same as [Dialog `onDismiss`](#dialog-ondismiss).

### DialogInner

Low-level component if you need direct access to the portaled dialog wrapper.

Note: Must be rendered inside of a `DialogWrapper`.

#### DialogInner Props

All props for [`DialogOverlay`](#dialogoverlay-props) can also be passed to `DialogInner`.

### DialogContent

Low-level component if you need more control over the styles or rendering of the dialog content.

Note: Must be a child of `DialogOverlay`.

Note: You only need to use this when you are also styling `DialogOverlay`, otherwise you can use the high-level `Dialog` component and pass the props to it. Any props passed to `Dialog` component (besides `isOpen` and `onDismiss`) will be spread onto `DialogContent`.

```jsx
// jsx-demo
function Example(props) {
	const [showDialog, setShowDialog] = React.useState(false);
	const buttonRef = React.useRef();
	const open = () => setShowDialog(true);
	const close = () => setShowDialog(false);

	return (
		<div>
			<button onClick={open}>Show Dialog</button>

			<DialogOverlay isOpen={showDialog} onDismiss={close}>
				<DialogContent
					style={{
						border: "solid 5px hsla(0, 0%, 0%, 0.5)",
						borderRadius: "10px",
					}}
				>
					<p>I have a nice border now.</p>
					<p>
						Note that we could have used the simpler <code>Dialog</code>{" "}
						instead.
					</p>
					<button onClick={close}>Got it.</button>
				</DialogContent>
			</DialogOverlay>
		</div>
	);
}
```

#### DialogContent CSS Selectors

Please see the [styling guide](/styling).

Use the following CSS to target the overlay:

```css
[data-reach-dialog-content] {
}
```

#### DialogContent Props

| Prop                                    | Type   | Required |
| --------------------------------------- | ------ | -------- |
| [`div` props](#dialogcontent-div-props) |        |          |
| [`children`](#dialogcontent-children)   | `node` | true     |

##### DialogContent `div` props

Any props not listed above will be spread onto the underlying `div`.

```jsx
<DialogContent className="nice-border">
	<p>The underlying element will receive a class</p>
</DialogContent>
```

##### DialogContent `children`

`children: React.ReactNode`

Accepts any renderable content.

```jsx
<DialogContent>
	<p>Anything you want, you got it.</p>
</DialogContent>
```

## Animation Example

If you'd like to animate the content, give [React Spring](https://github.com/drcmda/react-spring) a shot.

```jsx
// jsx-demo
(() => {
	const AnimatedDialogOverlay = animated(DialogOverlay);
	const AnimatedDialogContent = animated(DialogContent);

	function Example(props) {
		const [showDialog, setShowDialog] = React.useState(false);
		const transitions = useTransition(showDialog, {
			from: { opacity: 0, y: -10 },
			enter: { opacity: 1, y: 0 },
			leave: { opacity: 0, y: 10 },
		});
		return (
			<div>
				<button onClick={() => setShowDialog(true)}>Show Dialog</button>
				{transitions(
					(styles, item) =>
						item && (
							<AnimatedDialogOverlay style={{ opacity: styles.opacity }}>
								<AnimatedDialogContent
									aria-labelledby="dialog-title"
									style={{
										transform: styles.y.to(
											(value) => `translate3d(0px, ${value}px, 0px)`
										),
										border: "4px solid hsla(0, 0%, 0%, 0.5)",
										borderRadius: 10,
									}}
								>
									<button onClick={() => setShowDialog(false)}>
										Close Dialog
									</button>
									<h2 id="dialog-title">Animated Dialog</h2>
									<p>React Spring makes it too easy!</p>
									<input type="text" />
									<br />
									<input type="text" />
									<button>Ayyyyyy</button>
								</AnimatedDialogContent>
							</AnimatedDialogOverlay>
						)
				)}
			</div>
		);
	}
	return <Example />;
})();
```

## Accessibility

### Tabbable Elements

It is recommended to have at least one tabbable element in the dialog content. Ideally the first element in the dialog is a close button. If no tabbable elements are found, the dialog content element itself will receive focus.

### Labeling

As with many other complex UI components, a dialog needs to be properly labeled to provide context for users with assistive technology such as screen readers. If a dialog is announced to the user without a label, it can be confusing and difficult to navigate.

There are two general approaches to labeling UI components on the web: `aria-label` and `aria-labelledby`. If the text is visible on screen, you should provide the label's HTML element with a unique `id` attribute. That `id` value is then given to an `aria-labelledby` attribute (or in React, an `aria-labelledby` prop) on the `Dialog`. With this context, the screen reader will announce whatever text is nested inside that ID'd markup as the title for the `Dialog`.

Alternatively, a design may not include a visible label on the screen, but this context is still important for non-sighted users. In this case, you need to provide an `aria-label` prop to your `Dialog` instead.

#### With `aria-label`

```jsx
function Example(props) {
	return (
		<Dialog aria-label="Warning about next steps">
			<p>This might take a while! Are you sure?</p>
			<button>Go Forward</button> <button>Go Back</button>
		</Dialog>
	);
}
```

#### With aria-labelledby

```jsx
function Example(props) {
	const labelId = `label--${useId(props.id)}`;
	return (
		<Dialog aria-labelledby={labelId}>
			<h1 id={labelId}>Next Steps</h1>
			<p>Follow these steps carefully!</p>
			<button>Go Forward</button> <button>Go Back</button>
		</Dialog>
	);
}
```

> NOTE: American users may see `aria-labelledby` and suspect a typo. In some regions (including the UK and Great Britain), the word is indeed spelled `labelled` instead of `labeled`. The correct HTML attribute is `aria-labelledby`.

For more details, see the [W3C recommendations for `aria-label`](https://www.w3.org/TR/wai-aria/#aria-label).

### Using `aria-hidden` for Other Elements

The aria role "dialog" has been problematic in the past with the virtual cursor, effectively hiding a lot of dialog content from screen reader users. Instead, Dialog will set `aria-hidden` on all nodes at the `document.body` root except for the currently active dialog. This traps the virtual cursor inside the dialog.

This is a little unusual for a React component to traverse the DOM that it isn't a part of. Care has been taken to restore the manipulated attributes back to their original values.

### Keyboard Accessibility

| key               | action                                   |
| ----------------- | ---------------------------------------- |
| <kbd>Escape</kbd> | Dismisses the dialog (if the app allows) |

## Z-Index Wars

Dialog does not set a `z-index`; it depends on the DOM order to be on top of the rest of the app (the overlay is inserted at the end of the document when it is opened). If your app is already battling in the Z-Index Wars, be sure to add a `z-index` to the `<Dialog>` or `<DialogOverlay>` that you render.
